import telebot
import socket
import threading
import os
import configparser
import re
from datetime import datetime
import sys
import dataclasses
import requests
import logging
import uvicorn
import api

EXPORTER_LOG = "/var/log/smart-box/log"

if os.environ.get("DEBUG"):
    logging.basicConfig(format='%(asctime)s - %(levelname)s - %(message)s', level=logging.DEBUG,
                        handlers=[
                            logging.FileHandler(EXPORTER_LOG), logging.StreamHandler(sys.stdout)
                        ])
else:
    logging.basicConfig(format='%(levelname)s - %(message)s', level=logging.INFO,
                        handlers=[
                            logging.FileHandler(EXPORTER_LOG), logging.StreamHandler(sys.stdout)
                        ])


@dataclasses.dataclass
class Environment:
    TG_TOKEN: str
    LISTEN_HOST: str
    LISTEN_PORT: int
    STAND_URL: str
    REG_KEY: str
    API_COOKIE: str
    ADMIN_UIDS: list
    USER_UIDS: list
    CONFIG_PATH: str
    SET_POLICY: bool
    ALLOW_IP: list

    ProductBase: list

    def __init__(self):
        self.ProductBase = list()
        self.USER_UIDS = list()
        self.ALLOW_IP = ["127.0.0.1"]
        self.ADMIN_UIDS = list()

        if os.environ.get("CONFIG_PATH"):
            self.CONFIG_PATH = os.environ.get("CONFIG_PATH")
        else:
            self.CONFIG_PATH = "config.ini"

        try:
            config = configparser.ConfigParser()
            config.read(self.CONFIG_PATH)

            self.TG_TOKEN = config["Telegram"]["Bot_Token"]
            self.LISTEN_HOST = config["Socket"]["IP"]
            self.LISTEN_PORT = int(config["Socket"]["Port"])
            self.STAND_URL = config["Sign_API"]["Stand_Url"]
            self.REG_KEY = config["Sign_API"]["RegKey"]
            self.API_COOKIE = config["Sign_API"]["Cookie"]
            self.ADMIN_UIDS.append(config["Bot_Settings"]["Chief_ID"])
            self.SET_POLICY = bool(config["Bot_Settings"]["IP_Policy"])

            logging.info("Variables defined from config file")
        except Exception as e:
            logging.critical(f"Failed to read config file. Exit by - {e}")
            exit(1)


@dataclasses.dataclass
class TelegramBot:
    BOT: telebot

    def __init__(self, env: Environment):
        self.BOT = telebot.TeleBot(env.TG_TOKEN)
        logging.info("Bot has been initialized")

    def listen_chat(self, env: Environment):
        @self.BOT.message_handler(func=lambda message: True)
        def echo(message):
            if str(message.from_user.id) in env.ADMIN_UIDS:  # Обработка сообщений от админов
                self.BOT.send_message(message.from_user.id, self.__chat_admin(message.json["text"], env))
                os.system("sleep 0.3")
            elif str(message.from_user.id) in env.USER_UIDS:  # Обработка сообщений от пользователей
                self.BOT.send_message(message.from_user.id, self.__chat_user(message.json["text"], env))
                os.system("sleep 0.3")

        self.BOT.infinity_polling()

    def __chat_admin(self, message, env: Environment):
        global IP_POLICY, ALLOW_IP, KILL_SOCKET, DB

        if message[:7] == "/ip add":
            if env.SET_POLICY:
                if message[8:] not in ALLOW_IP:
                    env.ALLOW_IP.append(message[8:])
                    return "IP добавлен в список разрешенных"
                else:
                    return "IP уже в списке разрешенных"
            else:
                return "Функция фильтрации по IP адресам отключена"
        elif message[:6] == "/ip rm":
            if env.SET_POLICY:
                if message[7:] in env.ALLOW_IP:
                    env.ALLOW_IP.remove(message[7:])
                    return "IP удален из списка разрешенных"
                else:
                    return "IP нет в списке разрешенных"
            else:
                return "Функция фильтрации по IP адресам отключена"
        elif message == "/ip show":
            if env.SET_POLICY:
                return "Список разрешенных IP адресов:\n" + str(env.ALLOW_IP)
            else:
                return "Функция фильтрации по IP адресам отключена"
        elif message == "/ip allow":
            env.SET_POLICY = False
            return "Фильтрация отключена, разрешены подключения с любых IP адресов"
        elif message == "/ip deny":
            env.SET_POLICY = True
            return "Фильтрация включена, разрешены подключения с IP адресов находящихся в списке разрешенных"
        elif message == "/ip checkpolicy":
            if env.SET_POLICY:
                return "Фильтрация включена"
            else:
                return "Фильтрация отключена"
        elif message == "/role":
            return "Admin"
        elif message == "/help":
            return str("Взаимодействие с БД продкутов:\n"
                       "/product list - Вывод всей базы данных\n"
                       "/product search <ИМЯ_ПРОДУКТА> - Поиск по базе данных\n"
                       "/product add <ИМЯ_ПРОДУКТА> - Добавить продукт в базу данных\n"
                       "/product rm <ИМЯ_ПРОДУКТА> - Удалить продукт из базы данных\n"
                       "\nУправление правилами фильтрации:\n"
                       "/ip checkpolicy - проверить политику фильтрации IP адресов\n"
                       "/ip allow - разрешить подключение с любых IP адресов (ip add/show/rm будут неактивны)\n"
                       "/ip deny - запретить подключение с любых адресов (фильтрация на основе списка /ip show)\n"
                       "/ip show - показать список разрешенных IP адресов\n"
                       "/ip add <IP> - добавить IP в список разрешенных для подключения\n"
                       "/ip rm <IP> - удалить IP из списка разрешенных\n"
                       "\nУправление функциями приложения:\n"
                       "/socket kill - Остановить сетевой сокет (DEPRECATED)\n"
                       "\nИнформационные функции:\n"
                       "/help - Показ списка доступных команд\n"
                       "/role - Показать текущую роль")
        else:
            return self.__chat_user(message, env)

    def __chat_user(self, message, env: Environment):

        if message == "/role":
            return "User"
        elif message == "/help":
            return str("Взаимодействие с БД продкутов:\n"
                       "/product list - Вывод всей базы данных (DEPRECATED)\n"
                       "/product search <ИМЯ_ПРОДУКТА> - Поиск по базе данных (DEPRECATED)\n"
                       "/product add <ИМЯ_ПРОДУКТА> - Добавить продукт в базу данных (DEPRECATED)\n"
                       "/product rm <ИМЯ_ПРОДУКТА> - Удалить продукт из базы данных (DEPRECATED)\n"
                       "\nИнформационные функции:\n"
                       "/help - Показ списка доступных команд\n"
                       "/role - Показать текущую роль")
        else:
            return "Запрос не может быть обработан"


@dataclasses.dataclass
class API_TS:

    def __get_expireDate(self, product_code, env: Environment):

        headers = {'X-RegistrationKey': env.REG_KEY,
                   'Cookie': env.API_COOKIE}
        payload = {"codes": [product_code]}

        try:
            response = requests.post(env.STAND_URL, headers=headers, json=payload)
            if response.status_code == 200:
                if "codes" in response.json() and "expireDate" in response.json()["codes"][0]:
                    return response.json()["codes"][0]["expireDate"]
                else:
                    logging.info("No field found in the response - expireDate")
                    logging.info(response.json())
            else:
                logging.info(f"Request failed. Returned code - {response.status_code}")
                logging.info(response.json())
        except Exception:
            logging.info("Request processing error!")

    def __calc_out_date(self, exp_date):
        res = re.findall("([0-9]{4})-([0-9]{2})-([0-9]{2})", exp_date)
        try:
            date_now = datetime.now()
            exp_date_ref = datetime(int(res[0][0]), int(res[0][1]), int(res[0][2]))
            if date_now > exp_date_ref:
                return [">", str(date_now - exp_date_ref)]
            elif date_now < exp_date:
                return ["<", str(exp_date_ref - date_now)]
            elif date_now == exp_date:
                return ["="]
        except Exception:
            logging.info("Expiry date could not be processed")
            return False

    def check_product_code(self, socket_data_raw, env: Environment):
        try:
            socket_data = socket_data_raw.decode('utf-8')
        except Exception:
            logging.info("Data encoding could not be determined!")
            return False

        # Поиск продукта в БД ЧЗ
        exp_date = self.__get_expireDate(product_code=socket_data, env=env)
        if exp_date:
            res = self.__calc_out_date(exp_date)
            if res:
                if res[0] == ">":
                    return "Срок годности закончился - " + re.sub("days", "д.", str(res[1]))
                elif res[0] == "<":
                    return "Срок годности закончится через - " + re.sub("days", "д.", str(res[1]))
                elif res[0] == "=":
                    return "Срок годности заканчивается сегодня!"
                else:
                    logging.info("Date comparison error")
        else:
            logging.info("Date comparison error")


@dataclasses.dataclass
class ServerSocket:
    def socket_listen(self, env: Environment, api: API_TS, bot: TelegramBot):

        serv_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM, proto=0)
        serv_sock.bind((env.LISTEN_HOST, env.LISTEN_PORT))
        logging.info(f"Listening socket. Host - {env.LISTEN_HOST}, port - {env.LISTEN_PORT}")
        serv_sock.listen(10)

        while True:
            client_sock, client_addr = serv_sock.accept()
            logging.info(f"Client connecting to socket - {client_addr}")
            for i in env.ADMIN_UIDS:
                bot.BOT.send_message(i, "Уведомление: Подключен новый клиент к TCP - " + str(client_addr[0]) + ":" + str(
                    client_addr[1]))
            os.system("sleep 0.5")
            if client_addr[0] in env.ALLOW_IP or not env.SET_POLICY:

                while True:
                    data = client_sock.recv(100)

                    if not data:
                        logging.info("Client has disconnected")
                        for i in env.ADMIN_UIDS:
                            bot.BOT.send_message(i, "Уведомление: Клиент отключился")
                        os.system("sleep 0.5")
                        break
                    # Проверка принимаемых данных отключена т.к. неизвестна начальная последовательность
                    # if data[0:3] != b"010":
                    #     print("Закрыто соединение с плохим клиентом.")
                    #     print("Данные - " + data.decode("utf-8"))
                    #     for i in ADMIN_UIDs:
                    #         bot.send_message(i, "Уведомление: Закрыто соединение с плохим клиентом")
                    #     os.system("sleep 0.5")
                    #     break
                    else:
                        logging.info(f"Received data from client - {data}")
                        res = api.check_product_code(socket_data_raw=data, env=env)
                        if res:
                            for i in env.ADMIN_UIDS + env.USER_UIDS:
                                bot.BOT.send_message(i, res)

                client_sock.close()
            else:
                logging.info("Attempting to connect a non-existent client")
                for i in env.ADMIN_UIDS:
                    bot.BOT.send_message(i, "Уведомление: Неизвестный клиент. Отключаем...")
                client_sock.close()


def start_api():
    uvicorn.run(api.app, port=49151, host="0.0.0.0")


if __name__ == "__main__":
    try:
        env1 = Environment()

        tg_bot = TelegramBot(env1)
        api1 = API_TS()
        ss = ServerSocket()

        tg_listen_t = threading.Thread(target=tg_bot.listen_chat, args=[env1])
        tg_listen_t.setDaemon(True)
        tg_listen_t.start()

        api_t = threading.Thread(target=start_api)
        api_t.setDaemon(True)
        api_t.start()

        for i in env1.ADMIN_UIDS:
            tg_bot.BOT.send_message(i, "Start bot")

        ss.socket_listen(env=env1, api=api1, bot=tg_bot)

    except KeyboardInterrupt:
        print("Exit by KeyboardInterrupt")
        exit(0)

    except Exception as e:
        print(f"Exit by {e}")
        exit(1)
